TypeScript の Branded Type について少し学ぶ
幽霊型(Phantom Type)とは別なのか?
Nominal Type とも別?
Nominal Type(公称型)は TypeScript の Structual Type(構造的部分型) との対となる?概念だった
Phantom Type のモチベーションについては以下記事にある
TypeScript で幽霊型っぽいものをつくる
たとえば URL のエンコード。次の encode() 関数は、まだエンコードしてない文字列だけを受け取りたいとする。
code:ts
const encode(decoded: string) => encodeURIComponent(decoded);
しかし decoded はただの文字列でしかない
エンコード済みの文字列も渡すことができてしまい、多重エンコードが起きてしまう
未エンコードの文字列のみを受け取るように、コンパイラで検出できるようにしたい
これの実現のために幽霊型(Phantom Type)が用いられる
例えば Scala では
code:scala
class StrT (val str: String)
trait Normal
trait Encoded
def encode(x: StrNormal) = new StrEncoded(...)
のように記述できる
内部では利用されない型パラメータ(Normal, Encoded) を使って、Str[T] にはそういう種類があることを表現できる
また、Str[Normal] と Str[Encoded] は異なる型であるために互いに assign できないことも表現している
実装内部では一切使用されない型パラメータを使用してコンパイラレベルでのみ区別する方法が幽霊型(Phantom Type)
TypeScript だと素直にはできないのは構造的部分型(Structual Type)を採用しているため
code:ts
interface Str<T> { val: string; }; // Scala のマネ
let a: Str<Encoded>;
let b: Str<Normal>;
const encode = (s: Str<Normal>) => { ... };
encode(a); // OK
TypeScript で幽霊型(Phantom Type)を実現するためには「それっぽい」方法を取る必要がある
code:ts
interface ISO8601 extends String { __ISO8601: never };
const pattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|+-\d{2}:\d{2})$/;
export const iso8601 = (str: string): ISO8601 => {
if (pattern.test(str) throw Error("Not ISO8601");
return str as ISO8601;
};
iso("2026-02-17");
extends String が少しキモい
そこで string との intersection 型によって表現すると
code:ts
type ISO8601 = string & { __ISO8601: never };
const pattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|+-\d{2}:\d{2})$/;
export const iso8601 = (str: string): ISO8601 => {
if (pattern.test(str) throw Error("Not ISO8601");
return str as ISO8601;
};
iso("2026-02-17");